---
title: Architecture
summary: Layering, request path, shared process state, and integration points.
status: draft
owner: Not determined from source
last_reviewed: 2026-09-22
---

# Architecture

ADUserServices is a single-class, single-layer ASMX service: the codebehind is simultaneously the transport contract, the authorization gate, and the Active Directory data-access layer. There is no domain layer, no repository, no service/handler class, and no interface anywhere in the project.

## Hosting model

The web service runs in IIS. The `clsADUserServices.asmx` marker file names the codebehind class `ADUserServices.clsADUserServices`, which inherits `System.Web.Services.WebService`; ASP.NET creates an instance per request. The marker-file detail is on [Activation and hosting](activation-hosting.md); this page covers what happens once a request is dispatched.

| Aspect | Finding |
| --- | --- |
| Host | IIS (ASP.NET `System.Web.Services`) |
| Application class | None — no `Global.asax` exists anywhere in the repository |
| Startup work | None |
| Service base class | `System.Web.Services.WebService` |
| Instance lifetime | One instance per request (ASP.NET default) |
| Target framework | .NET Framework v4.6.2 |
| Language | VB.NET |

There is no constructor in the codebehind, so no per-request side work runs at instantiation — no log file is opened, no configuration is read, nothing is cached. `log4net 2.0.5` is referenced by the project but never initialized or called anywhere in source (a dead reference).

## Request path

The following diagram visualizes how a caller reaches an operation.

```mermaid
flowchart TD
    CALLER["SOAP caller"] --> IIS["IIS + ASP.NET"]
    IIS --> ASMX["clsADUserServices.asmx<br>WebService directive"]
    ASMX --> CTOR["clsADUserServices constructed<br>per request, no constructor work"]
    CTOR --> OPER["WebMethod invoked<br>clsADUserServices.OperationName"]
    OPER --> GATE{"IP allow-list gate<br>CheckAccess / mutating ops only"}
    GATE -->|"8 mutating operations"| MUT["Mutating AD operation"]
    GATE -->|"11 read operations<br>bypass the gate"| READ["Read-only AD query"]
    ENV["WebEnvironmentService<br>environment settings store"] --> GATE
    MUT --> ADEXT["Active Directory<br>external.saif.com writes"]
    READ --> ADEXT2["Active Directory<br>external.saif.com reads"]
    READ --> ADINT["Active Directory<br>corp.saif.com reads"]
    MUT --> DS["System.DirectoryServices /<br>System.DirectoryServices.AccountManagement"]
    READ --> DS
```

## Layer responsibilities

| Layer | Type | Responsibility |
| --- | --- | --- |
| `clsADUserServices.asmx` | `@WebService` directive | Maps the request address to the codebehind class |
| `ADUserServices.clsADUserServices` | `System.Web.Services.WebService` | Message handling, authorization gating, and direct Active Directory access — all in one class |

No helper/manager, repository/DAO, or embedded library exists; all directory access is concentrated in three private helpers (`GetContext`, `GetUserPrincipal`, `GetGroupPrincipal`) called from every operation, which is the one consistent seam in the design.

## Service declaration

| Service class | Attribute | Value |
| --- | --- | --- |
| `clsADUserServices` | `<WebService(Namespace:="http://tempuri.org/")>` | `http://tempuri.org/` |
| `clsADUserServices` | `<WebServiceBinding(ConformsTo:=WsiProfiles.BasicProfile1_1)>` | `WsiProfiles.BasicProfile1_1` |
| `clsADUserServices` | `<ToolboxItem(False)>` | Present |
| `clsADUserServices` | `<System.Web.Script.Services.ScriptService()>` | Commented out (not active) |
| `clsADUserServices` | `Inherits` | `System.Web.Services.WebService` |

## Shared process state

**No process-wide shared or `Shared`/`static` state exists on this class.** Only four per-request private instance fields are declared, none of which is `Shared`: `m_CredUserId` and `m_CredPass` are declared but never assigned or read anywhere in the codebehind (dead fields); `m_FoundUser` is written once (in `DoesUserHaveResource`) and never subsequently read; `m_LastMessage` captures directory-failure detail inside the private helpers (`GetContext`, `GetUserPrincipal`, `GetGroupPrincipal`) but is never returned to a caller or logged. Because every field is per-request instance state, no data survives between requests, no state is shared across concurrent callers, and nothing is lost on an application-pool recycle beyond the current in-flight request. This is recorded as a positive finding: there is no unsynchronized shared-state correctness or capacity risk of the kind seen in other ASMX hosts.

## Design constraints
- Every mutating operation hard-wires the external domain (`inExternalDomain = True` implicitly, via the repeated authored comment `'Assume external domain - NetSec will NOT use this`); there is no parameter to target the internal domain for writes.
- `GetUserPrinter` hard-wires the internal domain for its single read (authored comment `'Assume internal domain`) and offers no domain parameter.
- Three operations deviate from the `ADServiceResponse` sentinel convention used by the rest of the contract: `HelloWorld` returns a bare `String`, `DoesUserExist` returns a bare `Boolean`, and `GetUserGroups`/`GetUserResources` return an untyped `ArrayList` — so the contract carries four different response shapes for what is otherwise one pattern.
- The declared `WsiProfiles.BasicProfile1_1` conformance sits uncomfortably with the untyped `ArrayList` returns (serialized as unconstrained `anyType` arrays) and with the `Public Structure` message types whose *fields*, not properties, form the message body.

## Integration points

| System | Type | Purpose |
| --- | --- | --- |
| Active Directory (`external.saif.com`, container `DC=external,DC=saif,DC=com`) | Directory service | External-domain account provisioning, deletion, unlock, group membership, and attribute reads, via `System.DirectoryServices`/`System.DirectoryServices.AccountManagement` |
| Active Directory (`corp.saif.com`, container `DC=corp,DC=saif,DC=com`) | Directory service | Internal-domain attribute and membership reads only; the container constant is declared but never passed to `GetContext`, so internal-domain contexts are always built without a container |
| `WebEnvironmentService` (`SAIF.Framework.Security`, from `SAIF.BOF.Framework`) | Configuration/environment service | Reads the `aduserservices.allowedips` setting consumed by the `CheckAccess` gate |

No database, message queue, or downstream SOAP service is integrated anywhere in this codebase.

## Related documentation
- [Service index](index.md)
- [Overview](overview.md)
- [Activation and hosting](activation-hosting.md)
- [Operations](operations.md)
- [Faults and error pipeline](faults.md)
- [Authentication and authorization](authentication-authorization.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [Operational runbook](operational-runbook.md)
- [Tech debt](tech-debt.md)

## Sources
- `src\ADUserServices\clsADUserServices.asmx.vb`
- `.saifdocgen\state\runs\20260922-101326-aduserservices\analysis\aduserservices-analysis.md`
